package lang;

import editor.NotImplementedException;
import model.Project;
import model.code.*;
import model.code.line.SingleLine;
import model.symbol.Constant;
import model.symbol.LocalVariable;
import model.symbol.StructureMember;
import observer.ObservedList;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;

/**
 * This is the superclass of all language documentation comment parsing and generation classes.
 * It contains doc comment parsing and generation functionality common to all languages.  
 *
 * @author Neil Dickson
 */
public abstract class DocParser {
	/**
	 * The maximum number of characters in a line inside a generated doc comment (barring extreme cases)
	 * It is currently 32 tabstops (128) minus the starting tabstop for the box left side (4), minus 1 space for the box right side.
	 */
	public static final int INNER_CHAR_LIMIT = 123;

	// NOTE: These constants (except TYPE_DESCRIPTION) must be greater than 0 and less than 0x20, because they are stored as a char in a String
	public static final int TYPE_DESCRIPTION = 0;
	public static final int TYPE_PARAMETERS = 1;
	public static final int TYPE_LOCAL_VARS = 2;
	public static final int TYPE_RETURNS = 3;
	public static final int TYPE_MEMBERS = 4;
	public static final int TYPE_SEE_ALSO = 5;
	public static final int TYPE_AUTHORS = 6;
	public static final int TYPE_INCREMENT = 7;
	public static final int TYPE_LIBRARIES = 8;
	public static final int TYPE_INCLUDES = 9;

	public static final HashMap<String,Integer> HEADINGS = new HashMap<String, Integer>();
	static {
		HEADINGS.put("Parameters",TYPE_PARAMETERS);
		HEADINGS.put("Params",TYPE_PARAMETERS);
		HEADINGS.put("Param",TYPE_PARAMETERS);
		HEADINGS.put("Arguments",TYPE_PARAMETERS);
		HEADINGS.put("Args",TYPE_PARAMETERS);
		HEADINGS.put("Local Variables",TYPE_LOCAL_VARS);
		HEADINGS.put("Variables",TYPE_LOCAL_VARS);
		HEADINGS.put("Local Vars",TYPE_LOCAL_VARS);
		HEADINGS.put("Vars",TYPE_LOCAL_VARS);
		HEADINGS.put("Returns",TYPE_RETURNS);
		HEADINGS.put("Return",TYPE_RETURNS);
		HEADINGS.put("Members",TYPE_MEMBERS);
		HEADINGS.put("Content",TYPE_MEMBERS);
		HEADINGS.put("Member Variables",TYPE_MEMBERS);
		HEADINGS.put("See Also",TYPE_SEE_ALSO);
		HEADINGS.put("See",TYPE_SEE_ALSO);
		HEADINGS.put("Also",TYPE_SEE_ALSO);
		HEADINGS.put("Authors",TYPE_AUTHORS);
		HEADINGS.put("Author",TYPE_AUTHORS);
		HEADINGS.put("Increment",TYPE_INCREMENT);
		HEADINGS.put("Libraries",TYPE_LIBRARIES);
		HEADINGS.put("Includes",TYPE_INCLUDES);
		HEADINGS.put("Imports",TYPE_INCLUDES);
	}
	public static final HashMap<String,Class> CHUNK_TYPES = new HashMap<String, Class>();
	static {
		CHUNK_TYPES.put("function",Function.class);
		CHUNK_TYPES.put("procedure",Function.class);
		CHUNK_TYPES.put("method",Function.class);
		CHUNK_TYPES.put("structure",Structure.class);
		CHUNK_TYPES.put("file",CodeFile.class);
		CHUNK_TYPES.put("variable",GlobalVariable.class);
		CHUNK_TYPES.put("array",GlobalVariable.class);
		CHUNK_TYPES.put("global variable",GlobalVariable.class);
		CHUNK_TYPES.put("var",GlobalVariable.class);
		CHUNK_TYPES.put("global var",GlobalVariable.class);
		CHUNK_TYPES.put("constants",Constants.class);
		CHUNK_TYPES.put("enumeration",Constants.class);
		CHUNK_TYPES.put("section",Section.class);
		CHUNK_TYPES.put("segment",Section.class);
		CHUNK_TYPES.put("",CodeFileChunk.class);	// If no type, can be any CodeFileChunk
	}

	/**
	 * Takes a raw comment and transforms it to a uniform format with the guess of type at index 0 and guess of name at index 1
	 * @param originalDoc the raw comment text
	 * @return uniform format documentation, or null if determined to not be a doc comment, but no nulls in the String[]
	 */
	public abstract String[] docify(String[] originalDoc);

	/**
	 * Creates a doc comment for the specified {@link CodeFileChunk}.
	 * @param chunk the item for which to create a doc comment
	 * @return the formatted doc comment
	 */
	public abstract String createDocComment(CodeFileChunk chunk);

	/**
	 * Tries to add the documentation information in <code>doc</code> to the {@link CodeFileChunk}
	 * @param chunk the item to document
	 * @param doc the potential documentation
	 * @return true if the documentation was able to be matched with <code>chunk</code>
	 */
	public boolean addDocumentation(CodeFileChunk chunk,String[] doc) {
		// Check for right type and name (ignoring case and uninitialised values)
		Class chunkType = CHUNK_TYPES.get(doc[0].toLowerCase());
		if ((chunkType!=null && !chunkType.isInstance(chunk)) || (doc[1].length()>0 && chunk.getName().length()>0 && !chunk.getName().toString().equalsIgnoreCase(doc[1]))) {
			return false;
		}
		// If the chunk hasn't been named yet, name it. (most common for Constants)
		if (chunk.getName().length()==0 && doc[1].length()>0) {
			chunk.getName().replaceAll(doc[1]);
		}
		int i;
		// Get description
		StringBuffer description;
		if (doc.length>2 && (doc[2].length()==0 || doc[2].charAt(0)>=' '))  {
			description = new StringBuffer(doc[2]);
			i = 3;
		}
		else {
			description = new StringBuffer();
			i = 2;
		}
		for (;i<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');++i) {
			description.append('\n').append(doc[i]);
		}
		while (i<doc.length) {
			switch (doc[i].charAt(0)) {
				case TYPE_PARAMETERS: {
					if (!(chunk instanceof Function)) {
						return false;
					}
					Function function = (Function)chunk;
					ObservedList<LocalVariable> params = function.getParameters();
					for (++i;i+1<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');i+=2) {
						String name = doc[i];
						String desc = doc[i+1];
						boolean isFound = false;
						for (LocalVariable var : params) {
							if (var.getName().toString().equals(name)) {
								var.getDescription().replaceAll(desc);
								isFound = true;
								break;
							}
						}
						if (!isFound) {
							function.addParameter(new LocalVariable(name, Function.AUTOMATIC_PARAM_TYPE_TEXT,desc,function,CodeFileChunk.LOCATION_PARAMETERS,function.getParameters().size()));
						}
					}
					break;
				}
				case TYPE_LOCAL_VARS: {
					if (!(chunk instanceof Function)) {
						return false;
					}
					Function function = (Function)chunk;
					ObservedList<LocalVariable> vars = function.getLocalVariables();
					for (++i;i+1<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');i+=2) {
						String name = doc[i];
						String desc = doc[i+1];
						for (LocalVariable var : vars) {
							if (var.getName().toString().equals(name)) {
								var.getDescription().replaceAll(desc);
								break;
							}
						}
					}
					break;
				}
				case TYPE_RETURNS: {
					if (!(chunk instanceof Function)) {
						return false;
					}
					Function function = (Function)chunk;
					ObservedList<Function.Return> returns = function.getReturns();
					for (++i;i+1<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');i+=2) {
						String type = doc[i];
						String desc = doc[i+1];
						returns.add(new Function.Return(type.toCharArray(),desc.toCharArray(),function,returns.size()));
					}
					break;
				}
				case TYPE_MEMBERS: {
					if (!(chunk instanceof Structure) && !(chunk instanceof Constants)) {
						return false;
					}
					if (chunk instanceof Structure) {
						Structure structure = (Structure)chunk;
						ObservedList<StructureMember> members = structure.getMembers();
						for (++i;i+1<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');i+=2) {
							String name = doc[i];
							String desc = doc[i+1];
							for (StructureMember member : members) {
								if (member.getName().toString().equals(name)) {
									member.getDescription().replaceAll(desc);
									break;
								}
							}
						}
					}
					else { // chunk isntanceof Constants
						Constants constants = (Constants)chunk;
						ObservedList<Constant> members = constants.getMembers();
						for (++i;i+1<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');i+=2) {
							String name = doc[i];
							String desc = doc[i+1];
							for (Constant member : members) {
								if (member.getName().toString().equals(name)) {
									member.getDescription().replaceAll(desc);
									break;
								}
							}
						}
					}
					break;
				}
				case TYPE_SEE_ALSO: {
					ObservedList<Rowable> seeAlso = null;
					if (chunk instanceof Function) {
						seeAlso = ((Function)chunk).getSeeAlso();
					}
					else if (chunk instanceof Structure) {
						seeAlso = ((Structure)chunk).getSeeAlso();
					}
					else if (chunk instanceof Constants) {
						seeAlso = ((Constants)chunk).getSeeAlso();
					}
					else if (chunk instanceof GlobalVariable) {
						seeAlso = ((GlobalVariable)chunk).getSeeAlso();
					}
					else if (chunk instanceof Section) {
						seeAlso = ((Section)chunk).getSeeAlso();
					}
					if (seeAlso!=null) {
						for (++i;i<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');++i) {
							seeAlso.add(new Rowable(new SingleLine[]{new SingleLine(doc[i].toCharArray(), chunk, CodeFileChunk.LOCATION_SEE_ALSO, seeAlso.size()<<16)}));
						}
					}
					break;
				}
				case TYPE_AUTHORS: {
					if (chunk instanceof CodeFile) {
						CodeFile file = (CodeFile)chunk;
						ObservedList<Rowable> authors = file.getAuthors();
						for (++i;i<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');++i) {
							authors.add(new Rowable(new SingleLine[]{new SingleLine(doc[i].toCharArray(), chunk, CodeFileChunk.LOCATION_AUTHORS, authors.size()<<16)}));
						}
					}
					// Don't ignore the rest of the documentation if there's an author list specified; put it in the description
					else {
						description.append('\n').append("Authors:");
						for (++i;i<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');++i) {
							description.append("- ").append(doc[i]);
						}
					}
					break;
				}
				case TYPE_INCREMENT: {
					Constants constants = (Constants)chunk;
					constants.getIncrement().replaceAll(doc[i+1]);
					i+=2;
					break;
				}
				case TYPE_LIBRARIES: {
					if (!(chunk instanceof CodeFile)) {
						return false;
					}
					ObservedList<Project.ExternalInclude> libraries = chunk.getProject().getLibraryIncludes();
					for (++i;i+1<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');i+=2) {
						String name = doc[i];
						String desc = doc[i+1];
						for (Project.ExternalInclude library : libraries) {
							if (library.getName().toString().equals(name)) {
								library.getDescription().replaceAll(desc);
								break;
							}
						}
					}
					break;
				}
				case TYPE_INCLUDES: {
					if (!(chunk instanceof CodeFile)) {
						return false;
					}
					ObservedList<Project.ExternalInclude> includes = chunk.getProject().getImportedIncludes();
					for (++i;i+1<doc.length && (doc[i].length()==0 || doc[i].charAt(0)>=' ');i+=2) {
						String name = doc[i];
						String desc = doc[i+1];
						for (Project.ExternalInclude include : includes) {
							if (include.getName().toString().equals(name)) {
								include.getDescription().replaceAll(desc);
								break;
							}
						}
					}
					break;
				}
				default: {
					throw new NotImplementedException("Error in generated doc format!!! Unknown Special Documentation Metadata Marker: #"+((int)doc[i].charAt(0)));
				}
			}
		}
		chunk.setDescription(description.toString());
		return true;
	}

	/**
	 * The part of {@link #docify} that is common between languages
	 * @param doc text with box and trailing space (and possibly some leading space) removed
	 * @return standardised doc format text
	 */
	protected String[] docifyCommon(String[] doc) {
		ArrayList<String> finalDoc = new ArrayList<String>();
		// Trim the lines, since the code doesn't make use of the indenting and the definition/bulleted list functions need trimmed strings.
		for (int i=0;i<doc.length;++i) {
			doc[i] = doc[i].trim();
		}
		// Skip empty lines
		int i;
		for (i=0;i<doc.length && doc[i].length()==0;++i) {}
		// Check for title line
		if (i<doc.length && doc[i].matches("^\\s*[a-zA-Z][a-zA-Z ]+:(\\s+\\S.*)?$")) {
			int colonIndex = doc[i].indexOf(':');
			String type = doc[i].substring(0,colonIndex).trim();
			String name = doc[i].substring(colonIndex+1).trim();
			finalDoc.add(type);
			finalDoc.add(name);
			// Skip empty lines
			for (++i;i<doc.length && doc[i].length()==0;++i) {}
		}
		else {
			finalDoc.add("");
			finalDoc.add("");
		}
		ArrayList<String> metadata = new ArrayList<String>();
		String paragraph = null;
		for (;i<doc.length;++i) {
			String line = doc[i].trim();
			if (line.length()==0) {
				if (paragraph!=null) {
					finalDoc.add(paragraph);
					paragraph = null;
				}
			}
			else {
				int knownHeadingType = TYPE_DESCRIPTION;
				int colonIndex = line.indexOf(':');
				if (colonIndex>=0) {
					String heading = line.substring(0,colonIndex);
					if (HEADINGS.containsKey(heading)) {
						knownHeadingType = HEADINGS.get(heading);
					}
				}
				switch (knownHeadingType) {
					case TYPE_DESCRIPTION: {
						if (paragraph==null) {
							paragraph = line;
						}
						else {
							paragraph += " "+line;
						}
						break;
					}
					case TYPE_PARAMETERS:	// Falls through
					case TYPE_LOCAL_VARS:	// Falls through
					case TYPE_MEMBERS:		// Falls through
					case TYPE_RETURNS:		// Falls through
					case TYPE_LIBRARIES:	// Falls through
					case TYPE_INCLUDES: {
						int[] indexRef = new int[]{i+1};
						String[] definitions = extractDefinitionList(doc,indexRef);
						if (definitions.length>0) {
							metadata.add(""+((char)knownHeadingType));
							metadata.addAll(Arrays.asList(definitions));
						}
						i = indexRef[0]-1;
						break;
					}
					case TYPE_SEE_ALSO:		// Falls through
					case TYPE_AUTHORS: {
						int[] indexRef = new int[]{i+1};
						String[] list = extractBulletedList(doc,indexRef);
						if (list.length>0) {
							metadata.add(""+((char)knownHeadingType));
							metadata.addAll(Arrays.asList(list));
						}
						i = indexRef[0]-1;
						break;
					}
					case TYPE_INCREMENT: {
						String increment = line.substring(colonIndex+1).trim();
						if (increment.length()==0) {
							++i;
							increment = doc[i].trim();
						}
						metadata.add(""+((char)knownHeadingType));
						metadata.add(increment);
						break;
					}
				}
				if (knownHeadingType!=TYPE_DESCRIPTION && paragraph!=null) {
					finalDoc.add(paragraph);
					paragraph = null;
				}
			}
		}
		finalDoc.addAll(metadata);
		return finalDoc.toArray(new String[finalDoc.size()]);
	}

	/**
	 * Strips the bullets off of a doc comment bulleted list and merges multi-line bullets
	 * @param doc text with box and trailing/leading space removed
	 * @param index array whose element 0 is the real index
	 * @return the elements of the bulleted list
	 */
	private String[] extractBulletedList(String[] doc,int[] index) {
		ArrayList<String> list = new ArrayList<String>();
		while (index[0]<doc.length && doc[index[0]].length()!=0) {
			if ((doc[index[0]].length()==1 && doc[index[0]].startsWith("-")) || ((doc[index[0]].startsWith("-") || doc[index[0]].startsWith("*") || doc[index[0]].startsWith("o")) && (doc[index[0]].charAt(1)==' ' || doc[index[0]].charAt(1)=='\t'))) {
				// Take off the bullet
				list.add(doc[index[0]].substring(2).trim());
			}
			else if (!list.isEmpty()) {
				list.set(list.size()-1,list.get(list.size()-1)+" "+doc[index[0]]);
			}
			else {
				break;	// If there's text, but no "-", and no previous lines, the text is not a bulleted list.
			}
			++index[0];
		}
		String[] strings = new String[list.size()];
		return list.toArray(strings);
	}

	/**
	 * Splits terms and definitions of a doc comment definition list and merges multi-line definitions
	 * @param doc text with box and trailing/leading space removed
	 * @param index array whose element 0 is the real index
	 * @return an array where even indices hold terms and odd indices hold definitions
	 */
	private String[] extractDefinitionList(String[] doc,int[] index) {
		ArrayList<String> list = new ArrayList<String>();
		while (index[0]<doc.length && doc[index[0]].length()!=0) {
			int middle = doc[index[0]].indexOf("\t- ")+1;
			if (middle<=0) {
				middle = doc[index[0]].indexOf("\t-\t")+1;
			}
			middle = (middle!=0) ? middle : -1;
			if (middle<0 && doc[index[0]].indexOf("\t-")==doc[index[0]].length()-2) {
				middle = doc[index[0]].length()-1;
			}
			if (middle<0 && (doc[index[0]].startsWith("-\t") || doc[index[0]].startsWith("- "))) {
				middle = 0;
			}
			if (middle<0 && doc[index[0]].equals("-")) {
				middle = 0;
			}
			if (middle>=0) {
				// Split at bullet and omit bullet
				String term = doc[index[0]].substring(0,middle).trim();
				String definition = doc[index[0]].substring(middle+1).trim();
				list.add(term);
				list.add(definition);
			}
			else if (!list.isEmpty()) {
				list.set(list.size()-1,list.get(list.size()-1)+" "+doc[index[0]]);
			}
			else {
				break;	// If there's text, but no "-", and no previous lines, the text is not a definition list.
			}
			++index[0];
		}
		return list.toArray(new String[list.size()]);
	}

	/**
	 * The part of {@link #createDocComment} that is common between languages
	 * @param chunk the item for which to create a doc comment
	 * @return the text to go in the doc comment box, with lines at most {@link #INNER_CHAR_LIMIT} characters long (barring extreme cases)
	 */
	protected String[] createDocCommentCommon(CodeFileChunk chunk) {
		String titleLine = null;
		ArrayList<String> lines = new ArrayList<String>();
		lines.add("");
		makeDocLines(chunk.getDescription().toString(),lines);
		// If there was a description and it didn't end in a blank line, add a blank line.
		if (lines.size()>1 && lines.get(lines.size()-1).length()!=0) {
			lines.add("");
		}
		if (chunk instanceof CodeFile) {	// NOTE: This case must be before Section, because CodeFile extends Section.
			CodeFile file = (CodeFile)chunk;
			titleLine = "File: "+file.getName();
			ObservedList<Rowable> seeAlso = file.getSeeAlso();
			if (seeAlso!=null && seeAlso.size()>0) {
				makeDocBulletedList("See Also",seeAlso,lines);
				lines.add("");
			}
			ObservedList<Rowable> authors = file.getAuthors();
			if (authors!=null && authors.size()>0) {
				makeDocBulletedList("Author",authors,lines);
				lines.add("");
			}
			if (file.isMainFile()) {
				Project project = file.getProject();
				ObservedList<Project.ExternalInclude> libraryIncludes = project.getLibraryIncludes();
				if (libraryIncludes!=null && libraryIncludes.size()>0) {
					makeDocDefinitionList("Libraries",libraryIncludes, Project.ExternalInclude.NAME_INDEX, Project.ExternalInclude.DESCRIPTION_INDEX,lines);
					lines.add("");
				}
				ObservedList<Project.ExternalInclude> importedIncludes = project.getImportedIncludes();
				if (importedIncludes!=null && importedIncludes.size()>0) {
					makeDocDefinitionList("Includes",importedIncludes, Project.ExternalInclude.NAME_INDEX, Project.ExternalInclude.DESCRIPTION_INDEX,lines);
					lines.add("");
				}
			}
		}
		else if (chunk instanceof Section) {
			Section section = (Section)chunk;
			titleLine = "Section: "+section.getName();
			ObservedList<Rowable> seeAlso = section.getSeeAlso();
			if (seeAlso!=null && seeAlso.size()>0) {
				makeDocBulletedList("See Also",seeAlso,lines);
				lines.add("");
			}
		}
		else if (chunk instanceof Constants) {
			Constants constants = (Constants)chunk;
			titleLine = "Constants: "+constants.getName();
			ObservedList<Constant> members = constants.getMembers();
			if (members!=null && members.size()>0) {
				makeDocDefinitionList("Members",members,Constant.NAME_INDEX,Constant.DESCRIPTION_INDEX,lines);
				lines.add("");
			}
			String increment = constants.getIncrement().toString();
			if (increment.length()>0) {
				makeDocLines("Increment: "+increment,lines);
				lines.add("");
			}
			ObservedList<Rowable> seeAlso = constants.getSeeAlso();
			if (seeAlso!=null && seeAlso.size()>0) {
				makeDocBulletedList("See Also",seeAlso,lines);
				lines.add("");
			}
		}
		else if (chunk instanceof Structure) {
			Structure structure = (Structure)chunk;
			titleLine = "Structure: "+structure.getName();
			ObservedList<StructureMember> members = structure.getMembers();
			if (members!=null && members.size()>0) {
				makeDocDefinitionList("Members",members,StructureMember.NAME_INDEX,StructureMember.DESCRIPTION_INDEX,lines);
				lines.add("");
			}
			ObservedList<Rowable> seeAlso = structure.getSeeAlso();
			if (seeAlso!=null && seeAlso.size()>0) {
				makeDocBulletedList("See Also",seeAlso,lines);
				lines.add("");
			}
		}
		else if (chunk instanceof Function) {
			Function function = (Function)chunk;
			titleLine = "Function: "+function.getName();

			ObservedList<LocalVariable> parameters = function.getParameters();
			if (parameters!=null && parameters.size()>0) {
				makeDocDefinitionList("Parameters",parameters,LocalVariable.NAME_INDEX,LocalVariable.DESCRIPTION_INDEX,lines);
				lines.add("");
			}
			ObservedList<LocalVariable> localVariables = function.getLocalVariables();
			if (localVariables!=null && localVariables.size()>0) {
				makeDocDefinitionList("Local Variables",localVariables,LocalVariable.NAME_INDEX,LocalVariable.DESCRIPTION_INDEX,lines);
				lines.add("");
			}
			ObservedList<Function.Return> returns = function.getReturns();
			if (returns!=null && returns.size()>0) {
				makeDocDefinitionList("Local Variables",returns,Function.Return.TYPE_INDEX,Function.Return.DESCRIPTION_INDEX,lines);
				lines.add("");
			}
			ObservedList<Rowable> seeAlso = function.getSeeAlso();
			if (seeAlso!=null && seeAlso.size()>0) {
				makeDocBulletedList("See Also",seeAlso,lines);
				lines.add("");
			}
		}
		else if (chunk instanceof GlobalVariable) {
			GlobalVariable variable = (GlobalVariable)chunk;
			titleLine = "Variable: "+variable.getName();
			ObservedList<Rowable> seeAlso = variable.getSeeAlso();
			if (seeAlso!=null && seeAlso.size()>0) {
				makeDocBulletedList("See Also",seeAlso,lines);
				lines.add("");
			}
		}

		if (titleLine==null) {
			return null;
		}
		if (lines.get(lines.size()-1).length()==0) {
			lines.remove(lines.size()-1);
		}
		String[] doc = new String[lines.size()+1];
		doc[0] = titleLine;
		for (int i=0;i<lines.size();++i) {
			doc[i+1] = lines.get(i);
		}
		return doc;
	}

	private static void makeDocLines(String text,ArrayList<String> lines) {
		StringBuffer buf = new StringBuffer(INNER_CHAR_LIMIT);
		int prevTextIndex;
		int textIndex = 0;
		while (textIndex<text.length()) {
			buf.setLength(0);
			prevTextIndex = textIndex;
			while (text.charAt(prevTextIndex)==' ') {
				++prevTextIndex;
			}
			int textWidth = 0;
			while (textWidth<INNER_CHAR_LIMIT && textIndex<text.length() && text.charAt(textIndex)!='\r' && text.charAt(textIndex)!='\n') {
				if (text.charAt(textIndex)=='\t') {
					textWidth = (textWidth+4)&(~3);
				}
				else {
					++textWidth;
				}
				++textIndex;
			}
			// If at end of line (or just beyond from a tab), check text wrap cases
			if (textWidth>=INNER_CHAR_LIMIT && textIndex<text.length()) {
				int backupTextIndex = textIndex;
				int backupTextWidth = textWidth;
				while ((text.charAt(textIndex-1) > ' ') && (text.charAt(textIndex) > ' ') && (textIndex > prevTextIndex)) {
					--textIndex;
					--textWidth;
				}
				// Word is larger than entire line
				if ((text.charAt(textIndex-1) > ' ') && (text.charAt(textIndex) > ' ') && (textIndex==prevTextIndex)) {
					textIndex = backupTextIndex;
					textWidth = backupTextWidth;
				}
			}
			// TODO: Consider escaping extra whitespace so that it doesn't get lost.
			int textIndexNoTrailingSpaces = textIndex;
			while (text.charAt(textIndexNoTrailingSpaces-1)==' ') {
				--textIndexNoTrailingSpaces;
				--textWidth;
			}
			buf.append(text.substring(prevTextIndex,textIndexNoTrailingSpaces));
			lines.add(buf.toString());
			if (textIndex<text.length()) {
				// Check for ends of paragraphs
				if (text.charAt(textIndex)=='\r' || text.charAt(textIndex)=='\n') {
					++textIndex;
					lines.add("");
				}
				// Ignore multiple blank lines
				while (text.charAt(textIndex)=='\r' || text.charAt(textIndex)=='\n') {
					++textIndex;
				}
			}
		}
	}

	/**
	 * Makes a doc comment bulleted list
	 * @param title		title of the bulleted list, or null if none
	 * @param bullets	text for each bullet
	 * @param lines		the list of lines to which to add the bulleted list
	 */
	private void makeDocBulletedList(String title, Collection bullets,ArrayList<String> lines) {
		if (title!=null) {
			makeDocLines(title+":",lines);
		}
		for (Object bullet : bullets) {
			makeDocLines("\t- "+bullet,lines);
		}
	}

	/**
	 * Makes a doc comment definition list
	 * @param title		title of the definition list, or null if none
	 * @param content	list of {@link Rowable}s for the left and right column entries
	 * @param indexA	index
	 * @param indexB	list of {@link Rowable}s for the left and right column entries
	 * @param lines		the list of lines to which to add the definition list
	 */
	private void makeDocDefinitionList(String title,ObservedList<? extends Rowable> content,int indexA,int indexB,ArrayList<String> lines) {
		if (title!=null) {
			makeDocLines(title+":",lines);
		}
		int maxLength = 0;
		for (Rowable row : content) {
			maxLength = Math.max(maxLength,row.getStrings()[indexA].length());
		}
		for (Rowable row : content) {
			String line = row.getStrings()[indexA].toString();
			int textWidth = line.length();
			while (textWidth<=maxLength) {
				line += "\t";
				textWidth = (textWidth+4)&(~3);
			}
			line += "- "+row.getStrings()[indexB].toString();
			makeDocLines(line,lines);
		}
	}
}
